标准 IO
缓冲类型
标准 I/O 库中有三种缓冲概念:
缓冲类型 | 行为 |
---|---|
全缓冲 | 当缓冲区满时才执行 I/O 操作 |
行缓冲 | 遇到换行符或缓冲区满时执行 I/O 操作 |
无缓冲 | 立即执行 I/O 操作 |
使用 flush 函数可以立即刷新流,其具有以下特点:
终端驱动程序中的 flush 表示丢弃缓冲区的内容
当尝试从一个不带缓冲或行缓冲的流中输入数据时,会刷新所有 行缓冲 流
ISO C 做出了对缓冲特征的部分规定:
当且仅当 stdin 和 stdout 不指向交互式设备时,它们才是全缓冲的
stderr 绝对不会是全缓冲的
下面的特征是实现定义的:
stderr 是无缓冲的
指向终端的 stdin 和 stdout 是行缓冲的,否则为全缓冲
在多线程中缓冲可以会带来一些问题,以下面的代码为例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
printf("hello, world\n");
fork();
return 0;
}
在终端中直接执行的时候输出为一行内容,重定向到文件后输出为两行。原因是 stdio 默认是行缓冲的,而重定向到文件后,fork会把父进程缓冲区的内容也复制一份,在进程终止时被强制执行了缓冲区刷新
记录锁
记录锁允许进程对文件的一部分进行锁定,锁定的方式是读写锁。但是与读写锁不同的是,读写锁是用于线程同步的,加锁后如果想再加锁必须先解除以前的锁,而记录锁后面加的锁会解除前面的锁,另外
进程间的记录锁遵循读写锁的共享互斥原则
单一进程的新锁会覆盖旧锁
在文件上加锁时,系统对加锁区域根据条件进行合并或者分裂
记录锁的 API 如下:
int fcntl(int fd, int cmd, flock* flockptr);
参数说明如下:
flock 是一个结构体,其定义为:
struct flock{
short l_type;
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};
数据成员描述如下:
成员名 | 取值 | 说明 |
---|---|---|
l_type | F_RDLCK | 读锁 |
F_WRLCK | 写锁 | |
F_UNLCK | 解锁 | |
l_whence | SEEK_SET | |
SEEK_CUR | 当前位置 | |
SEEK_END | 文件末尾 | |
l_start | 指针偏移量 | |
l_len | 要锁定区域的长度 | |
l_pid | 进程 PID 为 l_pid 的进程持有的锁能阻塞当前进程 |
其中 l_pid 是一个输出变量,当 cmd = F_GETLK 时得到此变量,l_pid 代表了持有锁的进程(也是导致当前进程无法获得锁的进程)
当 l_len == 0 时,则表示从 l_whence + l_start 之后的所有区域,因此锁定一个文件的方法是另 l_whence = SEEK_SET, l_start = 0, l_len = 0
另外,锁可以越过文件末尾,但是无法超过开头(因为文件允许有空洞)
在使用 SEEK_END 时要额外注意,因为 SEEK_END 指向的始终是文件的末尾,你每次修改数据文件的末尾都会发生变化,相应地,文件指针的偏移量 l_start 也一直在变化 |
参数 cmd 的取值为:
取值 | 说明 |
---|---|
F_GETLK | 根据 flockptr 测试是否可以加锁,若可以加锁,则将 flockptr→l_type 设置为 F_UNLCK,否则将已有锁的信息复制到 flockptr |
F_SETLK | 根据 flockptr 尝试得到锁,若失败,则 fcntl 立即返回,并设 error 为 EAGAIN |
F_SETLK | 根据 flockptr 尝试得到锁,若无法得到锁则阻塞至成功。若 flockptr→l_type == F_UNLCK,则表示清除 flockptr 代表的锁 |
实际上 F_SETLK 的错误返回在规范中允许 error == EAGAIN 或 error == EACCESS,但是在实现中,总是令 error == EAGAIN |
另外,记录锁还有一些其它性质:
锁的生命周期取进程和文件生命周期的较小者。也就是说如果不手动释放锁,那么锁要么在文件关闭时被释放,要么在进程关闭时被释放
子进程不继承父进程的锁
在执行 exec 后,新程序继承原程序的锁
建议锁和强迫锁
如果我们的进程全部通过数据库进程访问数据,那么我们只需要在数据库进程上加锁就行了,但是外部程序可以自由地修改数据,这种锁称为建议锁
如果锁是由操作系统保证的,用户加完锁后其它用户的任意进程无法修改数据,就说这个锁是强迫锁
尽管操作系统会对文件加锁,但是不会对 unlink 进行限制,因此可以通过删除原有文件再创建新文件的方式绕过强迫锁
如果多个进程同时对一个不加锁的文件进行操作,那么文件的内容取决于最后退出的程序
文件锁
相比记录锁而言,文件锁更加简单易用:
int lockf(int fd, int cmd, off_t len);
int flock(int fd, int operation);
flock 和 lockf 不同之处在于 flock 是一个建议锁。对于 lockf 而言,fd 指明了需要操作的文件描述符,cmd 指明了操作,而 len 指明了从当前文件指针开始锁定的字节数量
cmd 可为:
参数 | 说明 |
---|---|
F_LOCK | 为文件的一段加锁 |
F_TLOCK | 尝试加锁 |
F_ULOCK | 解锁 |
F_TEST | 测试锁,如果文件中被测试的部分没有锁定或者是调用进程持有锁就返回 0;如果是其它进程持有锁就返回 -1,并且 errno 设置为 EAGAIN 或 EACCES。 |
文件锁一个可用的操作是进程之间通过创建锁文件来相互通信。在包管理器中经常会有这个用法 |
文件描述符和文件指针的相互转换
文件描述符转文件指针
FILE *fdopen(int fd, const char *mode);
文件指针转文件描述符:
int fileno(FILE *stream);